import { EventEmitter } from "events"
import { KeyboardEvent, ViewEvent } from "./event"
import { TextModel, SelectionModel } from "./models"
import {
  InsertTextOperation,
  SetSelectionOperation,
  RemoveTextOperation,
} from "./operations"
import Operation from "./operations/baseOperation"
import { ViewProvider, RenderView, SourcePreview, ViewMode } from "./view"

export type Plugin = (
  editor: Editor,
  op: Operation,
  originalApply: (op: Operation) => void,
) => (() => void) | void

const applyPlugins = () => {
  return (_target: Editor, _prop: string, description: any) => {
    const fn = description.value
    description.value = function (operation: Operation) {
      ;(this.plugins as Plugin[]).forEach((plugin, index) => {
        const cb = plugin(this, operation, fn.bind(this))

        if (!this.pluginsCbsMap.has(index) && cb) {
          this.pluginsCbsMap.set(index, cb)
        }
        return cb
      })

      fn.call(this, operation)
    }
  }
}

class Editor extends EventEmitter {
  private textModel_!: TextModel
  private selectionModel_!: SelectionModel
  private viewProvider!: ViewProvider
  private view_!: RenderView | SourcePreview
  private viewEventHandler!: ViewEvent
  private keyboardEventHandler!: KeyboardEvent
  private pluginsCbsMap: Map<1, (() => void) | void> = new Map()

  public undos?: () => void
  public redo?: () => void

  constructor(
    private container: HTMLElement,
    // @ts-ignore
    private plugins: Plugin[],
    private mode: ViewMode,
  ) {
    super()
    this.container = container
    this.viewProvider = new ViewProvider()
  }

  setMode = (mode: ViewMode) => {
    if (this.mode === "source" && mode === "source_preview") {
      this.view_ = this.viewProvider.provide(
        mode,
        this.textModel_,
        this.selectionModel_,
        this.container,
      )
      this.view_.render()
    } else if (this.mode === "source_preview" && mode === "source") {
      if ((this.view_ as SourcePreview).previewElement)
        (this.view_ as SourcePreview).previewElement!.style.display = "none"
    }
    this.mode = mode
  }

  init() {
    this.viewEventHandler = new ViewEvent(this, this.container)
    this.textModel_ = new TextModel()
    this.selectionModel_ = new SelectionModel(this.textModel_)
    this.view_ = this.viewProvider.provide(
      this.mode,
      this.textModel_,
      this.selectionModel_,
      this.container,
    )
    this.keyboardEventHandler = new KeyboardEvent(this, this.container)
    this.keyboardEventHandler.addListeners()
    this.viewEventHandler.addListeners()
  }

  @applyPlugins()
  apply(operation: Operation) {
    operation.apply(this)
  }

  clearPlugins() {
    this.plugins = []
    ;[...this.pluginsCbsMap.values()].forEach((cb) => cb?.())
  }

  dispose() {
    this.clearPlugins()
    this.keyboardEventHandler.removeListeners()
    this.viewEventHandler.removeListeners()
  }

  getView() {
    return this.view_
  }

  getTextModel() {
    return this.textModel_
  }

  getSelectionModel() {
    return this.selectionModel_
  }

  insertTextAtCursor(text: string) {
    const selection = this.selectionModel_.getSelection()
    let startIndex = selection?.anchor
    let cursorIndex: number = 0
    if (!this.selectionModel_.isCollapsed()) {
      const removeTextOperation = new RemoveTextOperation(
        selection.anchor,
        selection.focus,
      )
      this.apply(removeTextOperation)
      if (this.selectionModel_.isBackward()) {
        startIndex = selection.focus
      }
    }
    cursorIndex = startIndex + text.length
    const insertTextOperation = new InsertTextOperation(text, startIndex)

    this.apply(insertTextOperation)
    this.setSelection(cursorIndex, cursorIndex)
  }

  deleteTextAtCursor() {
    const selection = this.selectionModel_.getSelection()
    let startIndex = selection.anchor
    if (!this.selectionModel_.isCollapsed()) {
      this.apply(new RemoveTextOperation(selection.anchor, selection.focus))
      if (this.selectionModel_.isBackward()) {
        startIndex = selection.focus
      }
    } else if (selection.anchor > 0) {
      this.apply(new RemoveTextOperation(selection.anchor - 1, selection.focus))
      startIndex--
    }
    this.setSelection(startIndex, startIndex)
  }

  setSelection(anchor: number, focus: number) {
    const selectionOperaction = new SetSelectionOperation({ anchor, focus })
    this.apply(selectionOperaction)
  }
}

export default Editor
